package rad.framework.lang;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;

public final class ReflectionUtil {

	private ReflectionUtil() {
	}

	public static <T> T newInstance(final Class<T> type) {
		try {
			return type.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Class<?> forName(String name) {
		try {
			return Class.forName(name);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

	public static Method findMethodByName(Class type, String methodName) {
		Method[] methods = type.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method.getName().equals(methodName)
					&& Modifier.isPublic(method.getModifiers())) {
				return method;
			}
		}
		return null;
	}

	public static List<Method> findMethodsByAnnotation(Class type,
			Class annotation) {
		List<Method> list = new ArrayList<Method>();
		Method[] methods = type.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method.isAnnotationPresent(annotation)) {
				list.add(method);
			}
		}
		return list;
	}

	public static Method findMethodByArgs(Class type, String prefix, Class[] args) {
		Method[] methods = type.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method.getName().startsWith(prefix)) {
				Class[] params = method.getParameterTypes();
				if (params.length == args.length) {
					boolean found = true;
					for (int j = 0; j < params.length; j++) {
						if (!params[j].isAssignableFrom(args[j])) {
							found = false;
						}
					}
					if (found) {
						return method;
					}
				}
			}
		}
		return null;
	}

	public static Object invoke(Object object, String methodName, Object... args) {
		Method method = findMethodByName(object.getClass(), methodName);
		return invoke(method, object, args);
	}

	public static Object invoke(Method method, Object object, Object[] args) {
		try {
			return method.invoke(object, args);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static void add(Method getter, Object object, Object value) {
		Method method = findAdder(object.getClass(), getter.getName(), new Class[] { value.getClass() });
		if (method == null) {
			throw new RuntimeException("Adder not found for: " + getter.getName() + " on: " + object.getClass());
		}
		invoke(method, object, new Object[] { value });
	}
	
	protected static Method findAdder(Class type, String getter, Class[] args) {
		Method[] methods = type.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method.getName().startsWith("add")) {
				Class[] params = method.getParameterTypes();
				if (params.length == args.length) {
					boolean found = false;
					for (int j = 0; j < params.length; j++) {
						if (params[j].isAssignableFrom(args[j])
								&& method.getName().contains(
										getter.substring(3, getter.length()-3))) {
							found = true;
						}
					}
					if (found) {
						return method;
					}
				}
			}
		}
		return null;
	}

	public static Object getProperty(Object object, String name) {
		Method method = findMethodByName(object.getClass(), "get"
				+ StringUtils.capitalize(name));
		return invoke(method, object, new Object[] {});
	}

	public static void setProperty(Object object, String name, Object value) {
		try {
			BeanUtils.setProperty(object, name, value);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Map<String,Object> describe(Object object) {
		Map<String,Object> properties = new HashMap<String,Object>();
		if (object != null) {
			List<Method> list = new ArrayList<Method>();
			Method[] methods = object.getClass().getMethods();
			for (int i = 0; i < methods.length; i++) {
				Method method = methods[i];
				if (method.getName().startsWith("get")) {
					list.add(method);
				}
			}
			for (Method method : list) {
				String name = method.getName();
				name = name.substring(3);
				name = StringUtils.uncapitalize(name);
				properties.put(name, ReflectionUtil.invoke(method, object, new Object[] {}));
			}
		}
		return properties;
	}

	public static Class getParameterizedType(Class clazz) {
		return getParameterizedType(clazz, 0);
	}
	
	public static Class getParameterizedType(Class clazz, int index) {
		Type type = clazz.getGenericSuperclass();
		if (type == null) {
			throw new IllegalArgumentException(
					"Could not guess class by reflection");
		}
		if (type instanceof ParameterizedType) {
			ParameterizedType paramType = (ParameterizedType) type;
			return (Class) paramType.getActualTypeArguments()[index];
		} else {
			return getParameterizedType(clazz.getSuperclass());
		}
	}

	public static String getDeclaringClassAnnotation(Class type, Class annotationClass) {
		 if (type == null) {
			 return null;
		 }

		 List<Annotation> annotations = Arrays.asList(type.getDeclaredAnnotations());
		 Annotation annotation = type.getAnnotation(annotationClass);
		 
		 if (annotations.contains(annotation)) {
			 return type.getName();
		 }
		 else {
			 return getDeclaringClassAnnotation(type.getSuperclass(), annotationClass);
		 }
	 }
}
